独立开发日记:Widget 使用 CloudKit 中的数据
有时候想在 Widget 上显示 CloudKit 中的数据,如果直接在 Widget 中通过 Container 去获取数据是获取不到的。
开启 CloudKit

目前在新建工程时,如果钩上 Use Core Data 和 Host in CloudKit后,工程会默认生成 Persistence.swift 和 工程名.xcdatamodeld 两个文件。查看 Persistence.swift 可以发现是使用了 NSPersistentCloudKitContainer 来进行本地 Core Data 和 CloudKit 数据的同步和缓存。默认的 Core Data 保存数据的文件地址在 container.persistentStoreDescriptions.first。这个地址的数据并不会和 Widget 进行共享的。所以在 在 Widget 中通过 NSPersistentCloudKitContainer 是获取不到数据的。
App Group
为了解决这个问题,Apple 提供了 App Group 允许单个开发团队生成的多个应用程序访问共享容器并使用进程间通信 (IPC) 进行通信。应用程序可能属于一个或多个应用程序组。 开启 App Group 只要在 Capabilities 中添加 App Group 即可。
变更 CloudKit 本地数据地址
App Group 可以共享数据,那么解决方法就是把 CloudKit 的本地数据文件默认地址,放到 App Group 所在的文件夹下。
Cloud 数据未发布到正式环境
因为数据未发布到正式环境,所以开发数据没那么重要,只要在 Persistence.swift 中初始化 NSPersistentCloudKitContainer 时配置 NSPersistentStoreDescription Url 到 App Group 所在的文件夹下:
let container = NSPersistentContainer(name: "World")
let storeURL = AppGroup.facts.containerURL.appendingPathComponent("World.sqlite")
let description = NSPersistentStoreDescription(url: storeURL)
/// 1⃣️
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.xxxx.World")
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { ... }
Cloud 数据已经发布到正式环境
因为数据已发布到正式环境,所以数据是不允许丢失的。因此只能将数据库迁移。在 loadPersistentStores 完成后,通过 migratePersistentStore 将数据库迁移到新的 App Group 的地址:
container = NSPersistentCloudKitContainer(name: "World")
/// 迁移 coredata 并 cloudkit
var defaultURL: URL?
if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
}
let storeURL = AppGroup.world.containerURL.appendingPathComponent("World.sqlite")
if defaultURL == nil {
let description = NSPersistentStoreDescription(url: storeURL)
/// 1⃣️
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.xxxx.World")
container.persistentStoreDescriptions = [description]
}
container.loadPersistentStores(completionHandler: { [unowned container] _, error in
if let error = error as NSError? {
#if DEBUG
fatalError("Unresolved error \(error), \(error.userInfo)")
#endif
}
if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
let coordinator = container.persistentStoreCoordinator
if let oldStore = coordinator.persistentStore(for: url) {
do {
try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
} catch {
print(error.localizedDescription)
}
// delete old store
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
print(error.localizedDescription)
}
})
}
}
})
这里需要注意的是 1⃣️ 位置的代码,因为新的 NSPersistentStoreDescription 默认是没有配置 CloudKit 数据库,不会进行同步。所以需要配置 cloudKitContainerOptions 才能正常进行 Cloud 的数据同步。